Izpētiet uzlabotus atkarību injekcijas modeļus FastAPI, lai izveidotu mērogojamas, uzturamas un testējamas lietojumprogrammas. Uzziniet, kā strukturēt spēcīgu DI konteineru.
FastAPI Atkarību Injekcija: Uzlabota DI Konteineru Arhitektūra
FastAPI ar savu intuitīvo dizainu un jaudīgajām funkcijām ir kļuvis par iecienītu rīku mūsdienīgu tīmekļa API izveidei Python valodā. Viens no tā galvenajiem stiprumiem ir nevainojama integrācija ar atkarību injekciju (DI), kas ļauj izstrādātājiem izveidot vāji saistītas, testējamas un uzturamas lietojumprogrammas. Lai gan FastAPI iebūvētā DI sistēma ir lieliska vienkāršiem lietošanas gadījumiem, sarežģītākiem projektiem bieži vien ir nepieciešama strukturētāka un uzlabota DI konteineru arhitektūra. Šajā rakstā tiek pētītas dažādas stratēģijas šādas arhitektūras izveidei, sniedzot praktiskus piemērus un ieskatus spēcīgu un mērogojamu lietojumprogrammu projektēšanai.
Atkarību injekcijas (DI) un kontroles inversijas (IoC) izpratne
Pirms iedziļināties uzlabotās DI konteineru arhitektūrās, noskaidrosim pamatjēdzienus:
- Atkarību injekcija (DI): Dizaina modelis, kurā atkarības tiek nodrošinātas komponentam no ārējiem avotiem, nevis izveidotas iekšēji. Tas veicina vāju saistību, padarot komponentus vieglāk testējamus un atkārtoti izmantojamus.
- Kontroles inversija (IoC): Plašāks princips, kurā objektu izveides un pārvaldības kontrole tiek invertēta – deleģēta sistēmai vai konteineram. DI ir specifisks IoC veids.
FastAPI pēc būtības atbalsta DI, izmantojot savu atkarību sistēmu. Jūs definējat atkarības kā izsaucamus objektus (funkcijas, klases utt.), un FastAPI automātiski atrisina un injicē tos jūsu galapunktu funkcijās vai citās atkarībās.
Piemērs (pamata FastAPI DI):
from fastapi import FastAPI, Depends
app = FastAPI()
# Dependency
def get_db():
db = {"items": []} # Simulate a database connection
try:
yield db
finally:
# Close the database connection (if needed)
pass
# Endpoint with dependency injection
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
Šajā piemērā get_db ir atkarība, kas nodrošina datubāzes savienojumu. FastAPI automātiski izsauc get_db un injicē rezultātu (db vārdnīcu) read_items galapunkta funkcijā.
Kāpēc nepieciešams uzlabots DI konteiners?
FastAPI iebūvētā DI darbojas labi vienkāršiem projektiem, bet, lietojumprogrammām kļūstot sarežģītākām, sarežģītāks DI konteiners piedāvā vairākas priekšrocības:
- Centralizēta atkarību pārvaldība: Īpašs konteiners nodrošina vienotu patiesības avotu visām atkarībām, atvieglojot lietojumprogrammas atkarību pārvaldību un izpratni.
- Konfigurācijas un dzīves cikla pārvaldība: Konteiners var apstrādāt atkarību konfigurāciju un dzīves ciklu, piemēram, izveidot singletonus, pārvaldīt savienojumus un atbrīvoties no resursiem.
- Testējamība: Uzlabots konteiners vienkāršo testēšanu, ļaujot viegli ignorēt atkarības ar izspēles objektiem vai testa dubultniekiem.
- Atsaiste: Veicina lielāku komponentu atsaisti, samazinot atkarības un uzlabojot koda uzturēšanas spēju.
- Paplašināmība: Paplašināms konteiners ļauj pievienot pielāgotas funkcijas un integrācijas pēc vajadzības.
Stratēģijas uzlabota DI konteinera izveidei
Ir vairākas pieejas uzlabota DI konteinera izveidei FastAPI. Šeit ir dažas izplatītas stratēģijas:
1. Izmantojot īpašu DI bibliotēku (piemēram, `injector`, `dependency_injector`)
Python ir pieejamas vairākas jaudīgas DI bibliotēkas, piemēram, injector un dependency_injector. Šīs bibliotēkas nodrošina visaptverošu funkciju kopumu atkarību pārvaldībai, tostarp:
- Saistīšana: Definēšana, kā atkarības tiek atrisinātas un injicētas.
- Darbības jomas: Atkarību dzīves cikla kontrole (piemēram, singleton, īslaicīgs).
- Konfigurācija: Atkarību konfigurācijas iestatījumu pārvaldība.
- AOP (Aspect-Oriented Programming): Metožu izsaukumu pārtveršana šķērsgriezuma jautājumiem.
Piemērs ar `dependency_injector`
dependency_injector ir populāra izvēle DI konteineru izveidei. Ilustrēsim tā lietojumu ar piemēru:
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
# Define dependencies
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
# Initialize database connection
print(f"Connecting to database: {self.connection_string}")
def get_items(self):
# Simulate fetching items from the database
return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
class UserRepository:
def __init__(self, database: Database):
self.database = database
def get_all_users(self):
# Simulating database request to get all users
return [{"id": "user1", "name": "Alice"},{"id": "user2", "name": "Bob"}]
class Settings:
def __init__(self, database_url):
self.database_url = database_url
# Define container
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
settings = providers.Singleton(Settings, database_url = config.database_url)
database = providers.Singleton(Database, connection_string=config.database_url)
user_repository = providers.Factory(UserRepository, database=database)
# Create FastAPI app
app = FastAPI()
# Configure container (from an environment variable)
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__]) # enables injection of dependencies into FastAPI endpoints
# Dependency for FastAPI
def get_user_repository(user_repository: UserRepository = Depends(container.user_repository.provided)) -> UserRepository:
return user_repository
# Endpoint using injected dependency
@app.get("/users/")
async def read_users(user_repository: UserRepository = Depends(get_user_repository)):
return user_repository.get_all_users()
@app.on_event("startup")
async def startup_event():
# Container initialization
container.init_resources()
Paskaidrojums:
- Mēs definējam mūsu atkarības (
Database,UserRepository,Settings) kā parastas Python klases. - Mēs izveidojam
Containerklasi, kas manto nocontainers.DeclarativeContainer. Šī klase definē atkarības un to nodrošinātājus (piemēram,providers.Singletonsingletoniem,providers.Factoryjaunu instanču izveidei katru reizi). container.wire([__name__])rinda iespējo atkarību injekciju FastAPI galapunktos.get_user_repositoryfunkcija ir FastAPI atkarība, kas izmantocontainer.user_repository.provided, lai iegūtu UserRepository instanci no konteinera.- Galapunkta funkcija
read_usersinjicēUserRepositoryatkarību. configļauj jums eksternalizēt atkarību konfigurācijas. Pēc tam tas var nākt no vides mainīgajiem, konfigurācijas failiem utt.startup_eventtiek izmantots, lai inicializētu konteinerā pārvaldītos resursus.
2. Pielāgota DI konteinera ieviešana
Lai iegūtu lielāku kontroli pār DI procesu, varat ieviest pielāgotu DI konteineru. Šī pieeja prasa vairāk pūļu, bet ļauj pielāgot konteineru savām īpašajām vajadzībām.
Pamata pielāgota DI konteinera piemērs:
from typing import Callable, Dict, Type, Any
from fastapi import FastAPI, Depends
class Container:
def __init__(self):
self.dependencies: Dict[Type[Any], Callable[..., Any]] = {}
self.instances: Dict[Type[Any], Any] = {}
def register(self, dependency_type: Type[Any], provider: Callable[..., Any]):
self.dependencies[dependency_type] = provider
def resolve(self, dependency_type: Type[Any]) -> Any:
if dependency_type in self.instances:
return self.instances[dependency_type]
if dependency_type not in self.dependencies:
raise Exception(f"Dependency {dependency_type} not registered.")
provider = self.dependencies[dependency_type]
instance = provider()
return instance
def singleton(self, dependency_type: Type[Any], provider: Callable[..., Any]):
self.register(dependency_type, provider)
self.instances[dependency_type] = provider()
# Example Dependencies
class PaymentGateway:
def process_payment(self, amount: float) -> bool:
print(f"Processing payment of ${amount}")
return True # Simulate successful payment
class NotificationService:
def send_notification(self, message: str):
print(f"Sending notification: {message}")
# Example Usage
container = Container()
container.singleton(PaymentGateway, PaymentGateway)
container.singleton(NotificationService, NotificationService)
app = FastAPI()
# FastAPI Dependency
def get_payment_gateway(payment_gateway: PaymentGateway = Depends(lambda: container.resolve(PaymentGateway))):
return payment_gateway
def get_notification_service(notification_service: NotificationService = Depends(lambda: container.resolve(NotificationService))):
return notification_service
@app.post("/purchase/")
async def purchase_item(payment_gateway: PaymentGateway = Depends(get_payment_gateway), notification_service: NotificationService = Depends(get_notification_service)):
if payment_gateway.process_payment(100.0):
notification_service.send_notification("Purchase successful!")
return {"message": "Purchase successful"}
else:
return {"message": "Purchase failed"}
Paskaidrojums:
Containerklase pārvalda atkarību un to nodrošinātāju vārdnīcu.registermetode reģistrē atkarību ar tās nodrošinātāju.resolvemetode atrisina atkarību, izsaucot tās nodrošinātāju.singletonmetode reģistrē atkarību un izveido vienu tās instanci.- FastAPI atkarības tiek izveidotas, izmantojot lambda funkciju, lai atrisinātu atkarības no konteinera.
3. FastAPI `Depends` izmantošana ar rūpnīcas funkciju
Tā vietā, lai izmantotu pilnvērtīgu DI konteineru, varat izmantot FastAPI Depends kopā ar rūpnīcas funkcijām, lai sasniegtu zināmu atkarību pārvaldības līmeni. Šī pieeja ir vienkāršāka nekā pielāgota konteinera ieviešana, bet joprojām nodrošina dažas priekšrocības salīdzinājumā ar tiešu atkarību instanču izveidi galapunkta funkcijās.
from fastapi import FastAPI, Depends
from typing import Callable
# Define Dependencies
class EmailService:
def __init__(self, smtp_server: str):
self.smtp_server = smtp_server
def send_email(self, recipient: str, subject: str, body: str):
print(f"Sending email to {recipient} via {self.smtp_server}: {subject} - {body}")
# Factory function for EmailService
def create_email_service(smtp_server: str) -> EmailService:
return EmailService(smtp_server=smtp_server)
# FastAPI
app = FastAPI()
# FastAPI Dependency, leveraging factory function and Depends
def get_email_service(email_service: EmailService = Depends(lambda: create_email_service(smtp_server="smtp.example.com"))):
return email_service
@app.post("/send-email/")
async def send_email(recipient: str, subject: str, body: str, email_service: EmailService = Depends(get_email_service)):
email_service.send_email(recipient=recipient, subject=subject, body=body)
return {"message": "Email sent!"}
Paskaidrojums:
- Mēs definējam rūpnīcas funkciju (
create_email_service), kas izveidoEmailServiceatkarības instances. get_email_serviceatkarība izmantoDependsun lambda, lai izsauktu rūpnīcas funkciju un nodrošinātuEmailServiceinstanci.- Galapunkta funkcija
send_emailinjicēEmailServiceatkarību.
Uzlaboti apsvērumi
1. Darbības jomas un dzīves cikli
DI konteineri bieži nodrošina funkcijas atkarību dzīves cikla pārvaldībai. Izplatītākās darbības jomas ir:
- Singleton: Tiek izveidota viena atkarības instance un atkārtoti izmantota visā lietojumprogrammas darbības laikā. Tas ir piemērots atkarībām, kas ir bez stāvokļa vai ar globālu darbības jomu.
- Īslaicīgs: Katru reizi, kad tiek pieprasīts, tiek izveidota jauna atkarības instance. Tas ir piemērots atkarībām, kas ir ar stāvokli vai kurām jābūt izolētām viena no otras.
- Pieprasījums: Katram ienākošajam pieprasījumam tiek izveidota viena atkarības instance. Tas ir piemērots atkarībām, kurām jāuztur stāvoklis viena pieprasījuma kontekstā.
dependency_injector bibliotēka nodrošina iebūvētu atbalstu darbības jomām. Pielāgotiem konteineriem jums būs jāievieš darbības jomas pārvaldības loģika pašam.
2. Konfigurācija
Atkarībām bieži ir nepieciešami konfigurācijas iestatījumi, piemēram, datubāzes savienojuma virknes, API atslēgas un funkciju karodziņi. DI konteineri var palīdzēt pārvaldīt šos iestatījumus, nodrošinot centralizētu veidu, kā piekļūt un injicēt konfigurācijas vērtības.
dependency_injector piemērā config nodrošinātājs ļauj konfigurēt no vides mainīgajiem. Pielāgotiem konteineriem varat ielādēt konfigurāciju no failiem vai vides mainīgajiem un saglabāt tos konteinerā.
3. Testēšana
Viens no galvenajiem DI ieguvumiem ir uzlabota testēšanas spēja. Izmantojot DI konteineru, testēšanas laikā varat viegli aizstāt reālās atkarības ar izspēles objektiem vai testa dubultniekiem.
Piemērs (Testēšana ar `dependency_injector`):
import pytest
from unittest.mock import MagicMock
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
# Define dependencies (same as before)
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
def get_items(self):
return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
class UserRepository:
def __init__(self, database: Database):
self.database = database
def get_all_users(self):
return [{"id": "user1", "name": "Alice"},{"id": "user2", "name": "Bob"}]
class Settings:
def __init__(self, database_url):
self.database_url = database_url
# Define container (same as before)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
settings = providers.Singleton(Settings, database_url = config.database_url)
database = providers.Singleton(Database, connection_string=config.database_url)
user_repository = providers.Factory(UserRepository, database=database)
# Create FastAPI app (same as before)
app = FastAPI()
# Configure container (from an environment variable)
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__]) # enables injection of dependencies into FastAPI endpoints
# Dependency for FastAPI
def get_user_repository(user_repository: UserRepository = Depends(container.user_repository.provided)) -> UserRepository:
return user_repository
# Endpoint using injected dependency (same as before)
@app.get("/users/")
async def read_users(user_repository: UserRepository = Depends(get_user_repository)):
return user_repository.get_all_users()
@app.on_event("startup")
async def startup_event():
# Container initialization
container.init_resources()
# Test
@pytest.fixture
def test_client():
# Override the database dependency with a mock
database_mock = MagicMock(spec=Database)
database_mock.get_items.return_value = [{"id": 3, "name": "Test Item"}]
user_repository_mock = MagicMock(spec = UserRepository)
user_repository_mock.get_all_users.return_value = [{"id": "test_user", "name": "Test User"}]
# Override container with mock dependencies
container.user_repository.override(providers.Factory(lambda: user_repository_mock))
with TestClient(app) as client:
yield client
container.user_repository.reset()
def test_read_users(test_client: TestClient):
response = test_client.get("/users/")
assert response.status_code == 200
assert response.json() == [{"id": "test_user", "name": "Test User"}]
Paskaidrojums:
- Mēs izveidojam izspēles objektu
Databaseatkarībai, izmantojotMagicMock. - Mēs ignorējam
databasenodrošinātāju konteinerā ar izspēles objektu, izmantojotcontainer.database.override(). - Testa funkcija
test_read_itemstagad izmanto izspēles datubāzes atkarību. - Pēc testa izpildes tā atiestata konteinera ignorēto atkarību.
4. Asinhronās atkarības
FastAPI ir veidots, pamatojoties uz asinhrono programmēšanu (async/await). Strādājot ar asinhronām atkarībām (piemēram, asinhroniem datubāzes savienojumiem), pārliecinieties, vai jūsu DI konteiners un atkarību nodrošinātāji atbalsta asinhronās darbības.
Piemērs (Asinhronā atkarība ar `dependency_injector`):
import asyncio
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
# Define asynchronous dependency
class AsyncDatabase:
def __init__(self, connection_string: str):
self.connection_string = connection_string
async def connect(self):
print(f"Connecting to database: {self.connection_string}")
await asyncio.sleep(0.1) # Simulate connection time
async def fetch_data(self):
await asyncio.sleep(0.1) # Simulate database query
return [{"id": 1, "name": "Async Item 1"}]
# Define container
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
database = providers.Singleton(AsyncDatabase, connection_string=config.database_url)
# Create FastAPI app
app = FastAPI()
# Configure container
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__])
# Dependency for FastAPI
async def get_async_database(database: AsyncDatabase = Depends(container.database.provided)) -> AsyncDatabase:
await database.connect()
return database
# Endpoint using injected dependency
@app.get("/async-items/")
async def read_async_items(database: AsyncDatabase = Depends(get_async_database)):
data = await database.fetch_data()
return data
@app.on_event("startup")
async def startup_event():
# Container initialization
container.init_resources()
Paskaidrojums:
AsyncDatabaseklase definē asinhronās metodes, izmantojotasyncunawait.get_async_databaseatkarība ir definēta arī kā asinhronā funkcija.- Galapunkta funkcija
read_async_itemsir atzīmēta kāasyncun gaidadatabase.fetch_data()rezultātu.
Pareizās pieejas izvēle
Labākā pieeja uzlabota DI konteinera izveidei ir atkarīga no jūsu lietojumprogrammas sarežģītības un jūsu īpašajām prasībām:
- Maziem un vidēja lieluma projektiem: Var pietikt ar FastAPI iebūvēto DI vai rūpnīcas funkciju pieeju ar
Depends. - Lielākiem, sarežģītākiem projektiem: Īpaša DI bibliotēka, piemēram,
dependency_injector, nodrošina visaptverošu funkciju kopumu atkarību pārvaldībai. - Projektiem, kuriem nepieciešama precīza kontrole pār DI procesu: Pielāgota DI konteinera ieviešana var būt labākais risinājums.
Secinājums
Atkarību injekcija ir jaudīga metode mērogojamu, uzturamu un testējamu lietojumprogrammu izveidei. Lai gan FastAPI iebūvētā DI sistēma ir lieliska vienkāršiem lietošanas gadījumiem, uzlabota DI konteineru arhitektūra var sniegt ievērojamus ieguvumus sarežģītākiem projektiem. Izvēloties pareizo pieeju un izmantojot DI bibliotēku funkcijas vai ieviešot pielāgotu konteineru, varat izveidot spēcīgu un elastīgu atkarību pārvaldības sistēmu, kas uzlabo FastAPI lietojumprogrammu vispārējo kvalitāti un uzturēšanas spēju.
Globāli apsvērumi
Projektējot DI konteinerus globālām lietojumprogrammām, ir svarīgi ņemt vērā šādus aspektus:
- Lokalizācija: Ar lokalizāciju saistītās atkarības (piemēram, valodas iestatījumi, datumu formāti) jāpārvalda DI konteineram, lai nodrošinātu konsekvenci dažādos reģionos.
- Laika joslas: Atkarības, kas apstrādā laika joslu konvertēšanu, jāinjicē, lai izvairītos no laika joslu informācijas cietkoda.
- Valūta: Valūtas konvertēšanas un formatēšanas atkarības jāpārvalda konteineram, lai atbalstītu dažādas valūtas.
- Reģionālie iestatījumi: Citi reģionālie iestatījumi, piemēram, skaitļu formāti un adrešu formāti, arī jāpārvalda DI konteineram.
- Vairāku nomnieku atbalsts: Vairāku nomnieku lietojumprogrammām DI konteineram jāspēj nodrošināt dažādas atkarības dažādiem nomniekiem. To var panākt, izmantojot darbības jomas vai pielāgotu atkarību atrisināšanas loģiku.
- Atbilstība un drošība: Pārliecinieties, vai jūsu atkarību pārvaldības stratēģija atbilst attiecīgajiem datu privātuma noteikumiem (piemēram, GDPR, CCPA) un drošības paraugpraksei dažādos reģionos. Apstrādājiet sensitīvus akreditācijas datus un konfigurācijas droši konteinerā.
Apsverot šos globālos faktorus, varat izveidot DI konteinerus, kas ir labi piemēroti lietojumprogrammu izveidei, kas darbojas globālā vidē.